【SwiftUI】Firebase Remote Configを使って、アプリのコードを変更せずに画面を切り替えてみた
アプリの画面を気分によって変えたい時があっても、アプリのコードを変更するとApp Store Connectで審査提出しないといけないので骨が折れます。しかし、Firebase Remote Configを使うとアプリのコードを変更することなく簡単に画面を切り替えることが出来るのでその方法をSwiftUIで実践しつつ紹介していきます。今回は、Firebase Apple SDKがメジャーアップデートして9.0.0
がリリースされたということでそれを使っていきます。
作ったもの
Remote Configの値を変更して、アプリの見た目を変えています。
環境
- Xcode 13.3.1
- Firebase Apple SDK 9.0.0
Remote Configとは
Firebase Remote Config 公式ドキュメントから引用しますと、
アプリのアップデートを公開しなくても、アプリの動作と外観を変更できます。コストはかからず、1日あたりのアクティブ ユーザー数の制限もありません。
Firebaseコンソールで、アプリの動作や外観を制御するアプリ内デフォルト値を作成し、アプリはRemote Config バックエンドAPIを使用して、その値を反映することが出来ます。
準備
今回はFirebase Apple SDK 9.0.0
を使用する為、Xcodeの最小必要バージョンは13.3.1
となっております。
また、Firebaseプロジェクトが既に作成してある前提で進めていきます。
公式のFirebaseをAppleプロジェクトに追加するにプロジェクト作成手順が分かりやすく書いてありますので、まだの方はこちらを参考にしていただければと思います。
- Firebaseプロジェクトを作成する
- アプリを Firebase に登録する
- Firebase構成ファイルを取得し、アプリのルートディレクトリに配置する
ここまで準備が出来ましたら、アプリにFirebase SDKを追加します。
補足
Firebase Apple SDKは旧名iOS SDKから改名されましたが、以前の名残が残っており、所々でfirebase-ios-sdkになっております。紛らわしいので今回は必要な箇所以外はFirebase SDKと記載させていただきます。
アプリにFirebase SDKを追加
Xcodeで File > Add Packages... をクリックし開かれた画面の検索フォームにhttps://github.com/firebase/firebase-ios-sdk
を入力します。
入力すると、firebase-ios-sdkというパッケージが出てくるので、Dependency Rule
をUp to Next Major Version
を指定して、バージョンを9.0.0
にします。
Add Package
ボタンを押すとパッケージの追加処理が開始します。少し追加の処理に時間がかかるイメージがありますが気長に待ちください。
しばらくすると、 Firebase SDKのどのプロダクトを選択するかを決める画面が出てきます。
- FirebaseRemoteConfig
- FirebaseRemoteConfigSwift
今回は上記二つを選択して、Add Packageを行います。
FirebaseRemoteConfigSwift
は、9.0.0
になる前はベータ版だったのですが、9.0.0
からベータの記載が取り除かれたので試してみることにしました。FirebaseRemoteConfigSwift
を選択しなくても、Remote Config自体は何の問題もなく使用出来ます。
パッケージの追加が完了したのでアプリ側の実装に移ります。
アプリに実装する
AppDelegateを作成する
SwiftUIにはAppDelegate
クラスはデフォルトでは存在しない為、作成が必要です。
import SwiftUI import Firebase @main struct RemoteConfigPracticeApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() return true } } }
AppDelegate
を使用する為に、App
の構造体の中で@UIApplicationDelegateAdaptor(AppDelegate.self)
を用意し、AppDelegate
クラスを作成します。
アプリ起動時にFirebaseApp
共有インスタンスを構成したいのでAppDelegate
クラスのdidFinishLaunchingWithOptions
関数内で、FirebaseApp.configure()
を実行します。
RemoteConfigParameterを作成
今回はRemote Configのパラメータを扱うRemoteConfigParameter
を作成しました。
import Foundation import FirebaseRemoteConfig import FirebaseRemoteConfigSwift struct RemoteConfigParameter { /// 初期値のディクショナリー static let defaultConfigValues: [String: NSObject] = { var defaultDictionary = [String: NSObject]() for parameter in ParameterType.allCases { defaultDictionary.updateValue(parameter.defaultValue, forKey: parameter.key) } return defaultDictionary }() private let remoteConfig: RemoteConfig init(with remoteConfig: RemoteConfig) { self.remoteConfig = remoteConfig } /// Remote Configのパラメータタイプ enum ParameterType: String, CaseIterable { case isUnderMaintenance case maintenanceMessage var key: String { return self.rawValue } var defaultValue: NSObject { switch self { case .isUnderMaintenance: return false as NSObject case .maintenanceMessage: return "メンテナンス中" as NSObject } } } /// メンテナンス中かどうか var isUnderMaintenance: Bool { do { return try decodedValue(.isUnderMaintenance) } catch { return ParameterType.isUnderMaintenance.defaultValue as! Bool } } /// メンテナンスメッセージ var maintenanceMessage: String { do { return try decodedValue(.maintenanceMessage) } catch { return ParameterType.maintenanceMessage.defaultValue as! String } } // MARK: - Private func /// Remote Configで取得している値をデコードして返す /// - Parameter parameter: Remote Configパラメータタイプ /// - Returns: デコードされたRemote Configの値を返す private func decodedValue<T>(_ parameter: ParameterType) throws -> T { switch parameter { case .isUnderMaintenance: return try remoteConfig[parameter.key].decoded(asType: Bool.self) as! T case .maintenanceMessage: return try remoteConfig[parameter.key].decoded(asType: String.self) as! T } } }
プロパティや関数について説明していきます。
defaultConfigValues
Remote Configに設定する初期値です。
static let defaultConfigValues: [String: NSObject] = { var defaultDictionary = [String: NSObject]() for parameter in ParameterType.allCases { defaultDictionary.updateValue(parameter.defaultValue, forKey: parameter.key) } return defaultDictionary }()
enum ParameterType
にfor-in文でキー値とオブジェクトを持ったディクショナリを生成しています。
enum ParameterType
Remote Configで今回利用するパラメータに対してキー値とデフォルト値を取り出せるようにしています。
enum ParameterType: String, CaseIterable { case isUnderMaintenance case maintenanceMessage var key: String { return self.rawValue } var defaultValue: NSObject { switch self { case .isUnderMaintenance: return false as NSObject case .maintenanceMessage: return "メンテナンス中" as NSObject } } }
case isUnderMaintenance
は現在メンテナンス中かどうかを判定するパラメータで、case maintenanceMessage
はメンテナンス中に表示する文字列のパラメータになります。
isUnderMaintenance
メンテナンス中かどうかの変数になります。
var isUnderMaintenance: Bool { do { return try decodedValue(.isUnderMaintenance) } catch { return ParameterType.isUnderMaintenance.defaultValue as! Bool } }
decodedValue(_: ParameterType)
を呼んで値を取得しています。decodedValue
はthrows
付きの関数なのでエラーの場合はデフォルト値を返しています。
maintenanceMessage
メンテナンス中に表示する文字列です。
var maintenanceMessage: String { do { return try decodedValue(.maintenanceMessage) } catch { return ParameterType.maintenanceMessage.defaultValue as! String } }
isUnderMaintenance
と同様に、decodedValue(_: ParameterType)
で値を取得して、エラーの場合はデフォルト値を返しています。
decodedValue
RemoteConfigValue
をデコードした値を取得する関数です。
private func decodedValue<T>(_ parameter: ParameterType) throws -> T { switch parameter { case .isUnderMaintenance: return try remoteConfig[parameter.key].decoded(asType: Bool.self) as! T case .maintenanceMessage: return try remoteConfig[parameter.key].decoded(asType: String.self) as! T } }
渡されたParameterType
に紐づくRemoteConfigValue
に対してFirebaseRemoteConfigSwift
にあるRemoteConfigValue
のエクステンションメソッドdecoded(asType:)
を使用してデコードした値を返しています。
こちらはthrows
付きの関数になるので、デコードに失敗した場合はエラーを返します。
RemoteConfigState
Remote Configの状態を持っているObservableObject
です。
import SwiftUI import FirebaseRemoteConfig class RemoteConfigState: ObservableObject { /// fetchAndActivateを実施中ならtrue @Published var isFetchAndActivating = false private let remoteConfig: RemoteConfig private let remoteConfigParameter: RemoteConfigParameter // Remote Configの最小フェッチ間隔 private let minimumFetchInterval: TimeInterval = { #if DEBUG return 0 #else // デフォルト値 12時間(推奨) return 43200 #endif }() init() { remoteConfig = RemoteConfig.remoteConfig() let settings = RemoteConfigSettings() settings.minimumFetchInterval = minimumFetchInterval remoteConfig.configSettings = settings remoteConfig.setDefaults(RemoteConfigParameter.defaultConfigValues) remoteConfigParameter = RemoteConfigParameter(with: remoteConfig) fetchAndActive() } var isUnderMaintenance: Bool { return remoteConfigParameter.isUnderMaintenance } var maintenanceMessage: String { return remoteConfigParameter.maintenanceMessage } /// RemoteConfigの値をfetchしてactivateする private func fetchAndActive() { isFetchAndActivating = true Task { do { let _ = try await remoteConfig.fetchAndActivate() DispatchQueue.main.async { self.isFetchAndActivating = false } } catch { print(error.localizedDescription) } } } }
isFetchAndActivating
今回はFirebase Remote Configの読み込み方法にある読み込み画面の表示中に有効にする方法で実装します。
なので、読み込み中である場合は読み込み表示画面を表示する為の変数を用意しておきます。
@Published var isFetchAndActivating = false
この方法を使用する場合は、質の高いユーザーエクスペリエンスを提供するに読み込み画面にタイムアウトを追加することが推奨されています。が、今回は実装しておりません。実際にプロダクトコードとして書く時には使用したいですね。
A/Bテスト用の値を読み込む場合には、ユーザーがテストに参加してテスト値が適用されるまでの時間を短縮する為にも、この方法が強く推奨されています。
Firebase Remote Configの読み込み方法の次回の起動時に新しい値を読み込む方法を活用すれば、読み込み画面を表示する必要はなり、ユーザーの待機時間が大幅に短縮されますが、最新の状態になる為にアプリを最低2回起動する必要があります。
init
init() { remoteConfig = RemoteConfig.remoteConfig() let settings = RemoteConfigSettings() settings.minimumFetchInterval = minimumFetchInterval remoteConfig.configSettings = settings remoteConfig.setDefaults(RemoteConfigParameter.defaultConfigValues) remoteConfigParameter = RemoteConfigParameter(with: remoteConfig) fetchAndActive() }
settings.minimumFetchInterval
ではRemote Configの最小フェッチ間隔を設定しており、remoteConfig.setDefaults
ではRemote Configのパラメータの初期値をセットしています。最後にfetchAndActive()
でRemote Configの値をフェッチしてアクティベートしています。
settings.minimumFetchInterval
RemoteConfigバックエンドに対してフェッチ要求を再度行う前に経過する必要のある最小間隔になります。ここで設定した最小フェッチ間隔が経過するまで、バックエンドへの追加のフェッチ要求は許可されません。
Firebase Remote Config を使ってみるに記述があるのですが、
Remote Configのデフォルトのフェッチ間隔は 12時間であり、本番環境で推奨されるフェッチ間隔もこの値です。この場合、実際にフェッチ呼び出しが行われた回数に関係なく、12 時間の期間内で構成がバックエンドから複数回フェッチされることはありません。
ただ12時間の間、フェッチがされないとなると開発でのテストに困ってしまう為、開発環境ではこのminimumFetchInterval
の値を小さく設定することで頻繁なキャッシュの更新を行うことが出来ます。
このminimumFetchInterval
を小さく設定する方法は、開発目的でのみ使用し、本番環境で実行されるアプリには使用しないでくださいと記載されております。
今回は下記のようにDEBUGスキームの場合とそうでない場合でminimumFetchInterval
を変えています。
private let minimumFetchInterval: TimeInterval = { #if DEBUG return 0 #else // デフォルト値 12時間(推奨) return 43200 #endif }()
Xcodeでスキームを切り替える方法についてはこちらの記事を参考にしました。
remoteConfig.setDefaults
remoteConfig.setDefaults(RemoteConfigParameter.defaultConfigValues)
では、キー値に対してのデフォルト値をセットしています。
Remote Configが値を決定する方法は下記のようになっています。
- 最初にサーバーから保存されたキャッシュ値があるかどうかをチェックし、ある場合はそれを使用する
- キャッシュされた値がない場合は、
setDefaults()
で定義されたデフォルトを参照する - サーバーからキャッシュされた値がなく、デフォルトに値がない場合、サーバーはそのタイプのシステムデフォルトを使用する。
fetchAndActive()
Remote Configの値をフェッチしてアクティベートします。
private func fetchAndActive() { isFetchAndActivating = true Task { do { let _ = try await remoteConfig.fetchAndActivate() DispatchQueue.main.async { self.isFetchAndActivating = false } } catch { print(error.localizedDescription) } } }
まず、isFetchAndActivating
のフラグをtrue
にし、現在フェッチまたはアクティベート中であることを示します。
そして、try await remoteConfig.fetchAndActivate()
で値のフェッチとアクティベートを実行します。await
なので、fetchAndActivate()
に成功すると、self.isFetchAndActivating = false
に進み、失敗すると、catch
の中が呼ばれるようになっています。
今回はエラーハンドリングは行なっておりませんが、実際には適切なエラーハンドリングを行いましょう。
プロパティ
それぞれRemoteConfigParameter
から値を取得しています。
var isUnderMaintenance: Bool { return remoteConfigParameter.isUnderMaintenance } var maintenanceMessage: String { return remoteConfigParameter.maintenanceMessage }
ContentView
最後に見た目のViewになります。
import SwiftUI struct ContentView: View { @StateObject var remoteConfigState = RemoteConfigState() var body: some View { ZStack { if remoteConfigState.isUnderMaintenance { ZStack { Rectangle() .fill(.yellow) .ignoresSafeArea() Text(remoteConfigState.maintenanceMessage) } } else { Text("正常運転中!") } // // ? Remote Config読み込み中画面 // if remoteConfigState.isFetchAndActivating { ZStack { Rectangle() .fill(.gray.opacity(0.8)) .ignoresSafeArea() Text("Remote Config\n読み込み中") } } } } }
まず、remoteConfigState.isFetchAndActivating
がtrue
の場合は、読み込み中の画面が表示されます。読み込みが完了すると、remoteConfigState.isUnderMaintenance
フラグをみて、メンテナンス中であれば、メンテナンス中の画面が表示され、メンテナンス中でなければ*正常運転中!というテキストが表示されます。
おわりに
Remote Configで変更したものはリアルタイムで更新はされますが、本番環境での最小フェッチ間隔は12時間に設定することが推奨されています。なので、瞬時に画面を変更したい場合には活用するのは難しそうですね。12時間の間隔を考慮できるのであれば、アプリを変更してApp Store Connectに審査を提出するという流れを省く事ができるのでとても良いなと感じました。A/Bテストに活用するのも良さそうですね!
またせっかくの機会なのでFirebaseRemoteConfigSwift
を試してみたのですが、コンプリーションハンドラーの記載がなくなり、とてもスッキリしました。
今回少し登場したasync await
もこれから学んでいきたいと思います。